#ifdef PLATFORM_GLX

#include <GL/gl.h>
#include <GL/glx.h>

#else

#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>

#endif

#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>
#include <string>
#include <iostream>
#include <sstream>
#include <iomanip>

typedef std::basic_ostringstream<char> 	ostringstream;

#include "rendVect.hpp"
#include "rendVertAttr.hpp"

namespace st
{
#define SETUP_VERTEX_ATTR_POINTERS_MASK	(			\
		SETUP_VERTEX_ATTR_POINTERS_MASK_vertex |	\
		SETUP_VERTEX_ATTR_POINTERS_MASK_tcoord)

#include "rendVertAttr_setupVertAttrPointers.hpp"
#undef SETUP_VERTEX_ATTR_POINTERS_MASK

struct Vertex
{
	GLfloat pos[3];
	GLfloat txc[2];
};

} // namespace st

namespace mi
{
#define SETUP_VERTEX_ATTR_POINTERS_MASK	(			\
		SETUP_VERTEX_ATTR_POINTERS_MASK_vertex)

#include "rendVertAttr_setupVertAttrPointers.hpp"
#undef SETUP_VERTEX_ATTR_POINTERS_MASK

struct Vertex
{
	GLfloat pos[3];
};

} // namespace mi

#include "glyph.hpp"
#include "iterable.hpp"
#include "testbed.hpp"

namespace testbed
{

#define OPTION_IDENTIFIER_MAX	64

static const char arg_normal[]	= "normal_map";

static char g_normal_filename[FILENAME_MAX + 1] = "NMBalls.raw";
static unsigned g_normal_w = 256;
static unsigned g_normal_h = 256;

#ifndef PLATFORM_GLX

static EGLDisplay g_display = EGL_NO_DISPLAY;
static EGLContext g_context = EGL_NO_CONTEXT;

#endif

enum {
	TEX_NORMAL,
	TEX_ALBEDO,

	TEX_COUNT,
	TEX_FORCE_UINT = -1U
};

enum {
	PROG_PADDLE,
	PROG_BALL,

	PROG_COUNT,
	PROG_FORCE_UINT = -1U
};

enum {
	UNI_SAMPLER_NORMAL,
	UNI_SAMPLER_ALBEDO,

	UNI_MVP,
	UNI_SOLID_COLOR,

	UNI_COUNT,
	UNI_FORCE_UINT = -1U
};

static GLuint g_tex[TEX_COUNT];
static GLuint g_vbo[4];

static GLuint g_shader_vert[PROG_COUNT];
static GLuint g_shader_frag[PROG_COUNT];
static GLuint g_shader_prog[PROG_COUNT];

static GLint g_uni[PROG_COUNT][UNI_COUNT];

static unsigned g_num_faces[2];

static rend::ActiveAttrSemantics g_active_attr_semantics[PROG_COUNT];

static const int g_playfield_size = 80;
static const int g_paddle_size = 41;

static rend::ivect2 g_pos_paddle;
static rend::vect2 g_pos_ball;

static unsigned g_hits;
static unsigned g_score;
static unsigned g_score_step = 1;
static unsigned g_frame;

static const unsigned g_paddle_src_w = (g_paddle_size - 1) * 4;
static const unsigned g_paddle_src_h = 1;
static util::pix g_paddle_src[g_paddle_src_w];

enum {
	PADDLE_STATE_MUTABLE,
	PADDLE_STATE_IMMUTABLE	= int16_t(-1),
};

static int16_t g_paddle_state[g_paddle_src_w];

static Iterable< CooldownTarget, CooldownTransient > g_cooldown;
static Iterable< FalloutTarget, FalloutTransient >	 g_fallout;


bool
hook::set_num_drawcalls(
	const unsigned)
{
	return false;
}


unsigned
hook::get_num_drawcalls()
{
	return 1;
}


bool
hook::requires_depth()
{
	return false;
}


static bool
parse_cli(
	const unsigned argc,
	const char* const * argv)
{
	bool cli_err = false;

	const unsigned prefix_len = strlen(arg_prefix);

	#define QUOTE(x)	#x
	#define XQUOTE(x)	QUOTE(x)

	for (unsigned i = 1; i < argc && !cli_err; ++i)
	{
		if (strncmp(argv[i], arg_prefix, prefix_len) ||
			strcmp(argv[i] + prefix_len, arg_app))
		{
			continue;
		}

		if (++i < argc)
		{
			char option[OPTION_IDENTIFIER_MAX + 1];
			int opt_arg_start;

			if (1 == sscanf(argv[i], "%" XQUOTE(OPTION_IDENTIFIER_MAX) "s %n", option, &opt_arg_start))
			{
				if (!strcmp(option, arg_normal))
					if (3 == sscanf(argv[i] + opt_arg_start, "%" XQUOTE(FILENAME_MAX) "s %u %u",
								g_normal_filename, &g_normal_w, &g_normal_h))
					{
						continue;
					}
			}
		}

		cli_err = true;
	}

	#undef XQUOTE
	#undef QUOTE

	if (cli_err)
	{
		std::cerr << "app options (multiple args to an option must constitute a single string, eg. -foo \"a b c\"):\n"
			"\t" << testbed::arg_prefix << testbed::arg_app << " " << arg_normal <<
			" <filename> <width> <height>\t: use specified raw file and dimensions as source of normal map\n" << std::endl;
	}

	return !cli_err;
}


static bool
string_numeral_to_vertices(
	const char* str,
	const unsigned vb_len,
	mi::Vertex* vb,
	unsigned& nv,
	float& offset)
{
	while (unsigned i = *str++)
	{
		i -= '0';

		if (vb_len < nv + g_digit[i].ng * 2)
			return false;

		for (unsigned j = 0; j < g_digit[i].ng; ++j)
		{
			vb[nv].pos[0] = g_digit[i].vtx[j * 4 + 0] + offset;
			vb[nv].pos[1] = g_digit[i].vtx[j * 4 + 1];
			vb[nv].pos[2] = 0.f;
			++nv;
			vb[nv].pos[0] = g_digit[i].vtx[j * 4 + 2] + offset;
			vb[nv].pos[1] = g_digit[i].vtx[j * 4 + 3];
			vb[nv].pos[2] = 0.f;
			++nv;
		}

		offset += g_digit[i].width;
	}

	return true;
}


static bool
createIndexedFrame(
	const GLuint vbo_arr,
	const GLuint vbo_idx,
	unsigned& num_faces)
{
	const float thickness = 2.f / g_paddle_size;
	const float opening_extent = 1.f - thickness;

	const float quadrant[][2] =
	{
		{  1.f,  1.f },
		{ -1.f,  1.f },
		{ -1.f, -1.f },
		{  1.f, -1.f }
	};
	
	const unsigned sides = sizeof(quadrant) / sizeof(quadrant[0]);

	num_faces = sides * 2;

	static st::Vertex arr[sides * 4];
	unsigned ai = 0;

	const float paddle_texel = 1.f / g_paddle_src_w;

	for (unsigned i = 0; i < sides; ++i)
	{
		arr[ai].pos[0] = quadrant[i][0] * opening_extent;
		arr[ai].pos[1] = quadrant[i][1] * opening_extent;
		arr[ai].pos[2] = 0.f;
		arr[ai].txc[0] = (i * g_paddle_src_w / sides + 1) / float(g_paddle_src_w);
		arr[ai].txc[1] = .5f;

		++ai;

		arr[ai].pos[0] = quadrant[i][0];
		arr[ai].pos[1] = quadrant[i][1];
		arr[ai].pos[2] = 0.f;
		arr[ai].txc[0] = (i * g_paddle_src_w / sides + 0) / float(g_paddle_src_w);
		arr[ai].txc[1] = .5f;

		++ai;

		arr[ai].pos[0] = quadrant[(i + 1) % 4][0] * opening_extent;
		arr[ai].pos[1] = quadrant[(i + 1) % 4][1] * opening_extent;
		arr[ai].pos[2] = 0.f;
		arr[ai].txc[0] = ((i + 1) * g_paddle_src_w / sides + 0) / float(g_paddle_src_w);
		arr[ai].txc[1] = .5f;

		++ai;

		arr[ai].pos[0] = quadrant[(i + 1) % 4][0];
		arr[ai].pos[1] = quadrant[(i + 1) % 4][1];
		arr[ai].pos[2] = 0.f;
		arr[ai].txc[0] = ((i + 1) * g_paddle_src_w / sides + 1) / float(g_paddle_src_w);
		arr[ai].txc[1] = .5f;

		++ai;
	}

	assert(ai == sizeof(arr) / sizeof(arr[0]));

	static uint16_t idx[sides * 2][3];
	unsigned ii = 0;

	for (int i = 0; i < sides; ++i)
	{
		idx[ii][0] = (uint16_t)(i * 4 + 0) % ai;
		idx[ii][1] = (uint16_t)(i * 4 + 1) % ai;
		idx[ii][2] = (uint16_t)(i * 4 + 2) % ai;

		++ii;

		idx[ii][0] = (uint16_t)(i * 4 + 3) % ai;
		idx[ii][1] = (uint16_t)(i * 4 + 2) % ai;
		idx[ii][2] = (uint16_t)(i * 4 + 1) % ai;

		++ii;
	}

	assert(ii == sizeof(idx) / sizeof(idx[0]));

	const unsigned num_verts = sizeof(arr) / sizeof(arr[0]);
	const unsigned num_indes = sizeof(idx) / sizeof(idx[0][0]);

	std::cout << "number of vertices: " << num_verts << "\nnumber of indices: " << num_indes << std::endl;

	glBindBuffer(GL_ARRAY_BUFFER, vbo_arr);
	glBufferData(GL_ARRAY_BUFFER, sizeof(arr), arr, GL_STATIC_DRAW);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo_idx);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW);

	return true;
}


static bool
createIndexedGridQuad(
	const GLuint vbo_arr,
	const GLuint vbo_idx,
	unsigned& num_faces)
{
	assert(vbo_arr && vbo_idx);

	// thatch a grid with the following dimensions:
	const int rows = 2;
	const int cols = 2;

	// tile texture mapping the following number of times:
	const float tile = 1.f;

	assert(rows > 1);
	assert(cols > 1);

	static st::Vertex arr[rows * cols];
	unsigned ai = 0;

	num_faces = (rows - 1) * (cols - 1) * 2;

	for (int i = 0; i < rows; ++i)
		for (int j = 0; j < cols; ++j)
		{
			assert(ai < sizeof(arr) / sizeof(arr[0]));

			arr[ai].pos[0] = -1.f + j * (2.f / (cols - 1));
			arr[ai].pos[1] =  1.f - i * (2.f / (rows - 1));
			arr[ai].pos[2] = 0.f;
			arr[ai].txc[0] = tile * j / (cols - 1);
			arr[ai].txc[1] = tile * i / (rows - 1);

			++ai;
		}

	assert(ai == sizeof(arr) / sizeof(arr[0]));

	static uint16_t idx[(rows - 1) * (cols - 1) * 2][3];
	unsigned ii = 0;

	for (int i = 0; i < rows - 1; ++i)
		for (int j = 0; j < cols - 1; ++j)
		{
			idx[ii][0] = (uint16_t)(j + i * cols);
			idx[ii][1] = (uint16_t)(j + (i + 1) * cols);
			idx[ii][2] = (uint16_t)(j + i * cols + 1);

			++ii;

			idx[ii][0] = (uint16_t)(j + i * cols + 1);
			idx[ii][1] = (uint16_t)(j + (i + 1) * cols);
			idx[ii][2] = (uint16_t)(j + (i + 1) * cols + 1);

			++ii;
		}

	assert(ii == sizeof(idx) / sizeof(idx[0]));

	const unsigned num_verts = sizeof(arr) / sizeof(arr[0]);
	const unsigned num_indes = sizeof(idx) / sizeof(idx[0][0]);

	std::cout << "number of vertices: " << num_verts << "\nnumber of indices: " << num_indes << std::endl;

	glBindBuffer(GL_ARRAY_BUFFER, vbo_arr);
	glBufferData(GL_ARRAY_BUFFER, sizeof(arr), arr, GL_STATIC_DRAW);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo_idx);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW);

	return true;
}


static bool
check_context(
	const char* prefix)
{
	bool context_correct = true;

#ifndef PLATFORM_GLX

	if (g_display != eglGetCurrentDisplay())
	{
		std::cerr << prefix << " encountered foreign display" << std::endl;

		context_correct = false;
	}

	if (g_context != eglGetCurrentContext())
	{
		std::cerr << prefix << " encountered foreign context" << std::endl;

		context_correct = false;
	}

#endif

	return context_correct;
}


bool
hook::deinit_resources()
{
	if (!check_context(__FUNCTION__))
		return false;

	for (unsigned i = 0; i < sizeof(g_shader_prog) / sizeof(g_shader_prog[0]); ++i)
	{
		glDeleteProgram(g_shader_prog[i]);
		g_shader_prog[i] = 0;
	}

	for (unsigned i = 0; i < sizeof(g_shader_vert) / sizeof(g_shader_vert[0]); ++i)
	{
		glDeleteShader(g_shader_vert[i]);
		g_shader_vert[i] = 0;
	}

	for (unsigned i = 0; i < sizeof(g_shader_frag) / sizeof(g_shader_frag[0]); ++i)
	{
		glDeleteShader(g_shader_frag[i]);
		g_shader_frag[i] = 0;
	}

	glDeleteTextures(sizeof(g_tex) / sizeof(g_tex[0]), g_tex);
	memset(g_tex, 0, sizeof(g_tex));

	glDeleteBuffers(sizeof(g_vbo) / sizeof(g_vbo[0]), g_vbo);
	memset(g_vbo, 0, sizeof(g_vbo));

#ifndef PLATFORM_GLX

	g_display = EGL_NO_DISPLAY;
	g_context = EGL_NO_CONTEXT;

#endif

	return true;
}


bool
hook::init_resources(
	const unsigned argc,
	const char* const * argv)
{
	if (!parse_cli(argc, argv))
		return false;

#ifndef PLATFORM_GLX

	g_display = eglGetCurrentDisplay();

	if (EGL_NO_DISPLAY == g_display)
	{
		std::cerr << __FUNCTION__ << " encountered nil display" << std::endl;

		return false;
	}

	g_context = eglGetCurrentContext();

	if (EGL_NO_CONTEXT == g_context)
	{
		std::cerr << __FUNCTION__ << " encountered nil context" << std::endl;

		return false;
	}

#endif

	/////////////////////////////////////////////////////////////////

	glEnable(GL_CULL_FACE);
	glDisable(GL_DEPTH_TEST);

	GLclampf red	= .0625f;
 	GLclampf green	= .0625f;
 	GLclampf blue	= .0625f;
 	GLclampf alpha	= 1.f;

	glClearColor(red, green, blue, alpha);

	/////////////////////////////////////////////////////////////////

	glGenTextures(sizeof(g_tex) / sizeof(g_tex[0]), g_tex);

	for (unsigned i = 0; i < sizeof(g_tex) / sizeof(g_tex[0]); ++i)
		assert(g_tex[i]);

	testbed::util::fill_with_checker(
		g_paddle_src, 0,
		g_paddle_src_w,
		g_paddle_src_h, 1);

	for (unsigned i = 0; i < 4; ++i)
		g_paddle_src[i * g_paddle_src_w / 4] = util::pix(255, 255, 255);

	if (!util::setupTexture2D(g_tex[TEX_ALBEDO], g_paddle_src, g_paddle_src_w, g_paddle_src_h))
	{
		std::cerr << __FUNCTION__ << " failed at setupTexture2D" << std::endl;

		deinit_resources();

		return false;
	}

	for (unsigned i = 0; i < 4; ++i)
		g_paddle_state[i * g_paddle_src_w / 4] = PADDLE_STATE_IMMUTABLE;

	/////////////////////////////////////////////////////////////////

	for (unsigned i = 0; i < PROG_COUNT; ++i)
		for (unsigned j = 0; j < UNI_COUNT; ++j)
			g_uni[i][j] = -1;

	/////////////////////////////////////////////////////////////////

	g_shader_vert[PROG_PADDLE] = glCreateShader(GL_VERTEX_SHADER);
	assert(g_shader_vert[PROG_PADDLE]);

	if (!util::setupShader(g_shader_vert[PROG_PADDLE], "mvp_texture.glslv"))
	{
		std::cerr << __FUNCTION__ << " failed at setupShader" << std::endl;

		deinit_resources();

		return false;
	}

	g_shader_frag[PROG_PADDLE] = glCreateShader(GL_FRAGMENT_SHADER);
	assert(g_shader_frag[PROG_PADDLE]);

	if (!util::setupShader(g_shader_frag[PROG_PADDLE], "texture.glslf"))
	{
		std::cerr << __FUNCTION__ << " failed at setupShader" << std::endl;

		deinit_resources();

		return false;
	}

	g_shader_prog[PROG_PADDLE] = glCreateProgram();
	assert(g_shader_prog[PROG_PADDLE]);

	if (!util::setupProgram(
			g_shader_prog[PROG_PADDLE],
			g_shader_vert[PROG_PADDLE],
			g_shader_frag[PROG_PADDLE]))
	{
		std::cerr << __FUNCTION__ << " failed at setupProgram" << std::endl;

		deinit_resources();

		return false;
	}

	g_uni[PROG_PADDLE][UNI_MVP] = glGetUniformLocation(g_shader_prog[PROG_PADDLE], "mvp");

	g_uni[PROG_PADDLE][UNI_SAMPLER_NORMAL] = glGetUniformLocation(g_shader_prog[PROG_PADDLE], "normal_map");
	g_uni[PROG_PADDLE][UNI_SAMPLER_ALBEDO] = glGetUniformLocation(g_shader_prog[PROG_PADDLE], "albedo_map");

	g_active_attr_semantics[PROG_PADDLE].registerVertexAttr(glGetAttribLocation(g_shader_prog[PROG_PADDLE], "at_Vertex"));
	g_active_attr_semantics[PROG_PADDLE].registerTCoordAttr(glGetAttribLocation(g_shader_prog[PROG_PADDLE], "at_MultiTexCoord0"));

	/////////////////////////////////////////////////////////////////

	g_shader_vert[PROG_BALL] = glCreateShader(GL_VERTEX_SHADER);
	assert(g_shader_vert[PROG_BALL]);

	if (!util::setupShader(g_shader_vert[PROG_BALL], "mvp.glslv"))
	{
		std::cerr << __FUNCTION__ << " failed at setupShader" << std::endl;

		deinit_resources();

		return false;
	}

	g_shader_frag[PROG_BALL] = glCreateShader(GL_FRAGMENT_SHADER);
	assert(g_shader_frag[PROG_BALL]);

	if (!util::setupShader(g_shader_frag[PROG_BALL], "basic.glslf"))
	{
		std::cerr << __FUNCTION__ << " failed at setupShader" << std::endl;

		deinit_resources();

		return false;
	}

	g_shader_prog[PROG_BALL] = glCreateProgram();
	assert(g_shader_prog[PROG_BALL]);

	if (!util::setupProgram(
			g_shader_prog[PROG_BALL],
			g_shader_vert[PROG_BALL],
			g_shader_frag[PROG_BALL]))
	{
		std::cerr << __FUNCTION__ << " failed at setupProgram" << std::endl;

		deinit_resources();

		return false;
	}

	g_uni[PROG_BALL][UNI_MVP]			= glGetUniformLocation(g_shader_prog[PROG_BALL], "mvp");
	g_uni[PROG_BALL][UNI_SOLID_COLOR]	= glGetUniformLocation(g_shader_prog[PROG_BALL], "solid_color");

	g_active_attr_semantics[PROG_BALL].registerVertexAttr(glGetAttribLocation(g_shader_prog[PROG_BALL], "at_Vertex"));

	/////////////////////////////////////////////////////////////////

	glGenBuffers(sizeof(g_vbo) / sizeof(g_vbo[0]), g_vbo);

	for (unsigned i = 0; i < sizeof(g_vbo) / sizeof(g_vbo[0]); ++i)
		assert(g_vbo[i]);

	createIndexedFrame(
		g_vbo[0], g_vbo[1], g_num_faces[0]);

	createIndexedGridQuad(
		g_vbo[2], g_vbo[3], g_num_faces[1]);

	return !util::reportGLError();
}


bool
hook::render_frame()
{
	if (!check_context(__FUNCTION__))
		return false;

	CooldownTarget bg;

	if (g_cooldown(bg))
		glClearColor(bg.r, bg.g, bg.b, bg.a);

	glClear(GL_COLOR_BUFFER_BIT);

	/////////////////////////////////////////////////////////////////

	const float denom_pfield = 1.f / g_playfield_size;
	const float denom_paddle = g_paddle_size * denom_pfield;

	const rend::vect2 pos_pa = rend::vect2(g_pos_paddle[0], g_pos_paddle[1]).mul(2.f * denom_pfield);
	const rend::vect2 pos_ba = rend::vect2(g_pos_ball).mul(2.f * denom_pfield);

	const rend::matx4 mv_pa = rend::matx4().scale(denom_paddle, denom_paddle, 1.f).mul(rend::matx4().translate(pos_pa[0], pos_pa[1], -2.f));
	const rend::matx4 mv_ba = rend::matx4().scale(denom_pfield, denom_pfield, 1.f).mul(rend::matx4().translate(pos_ba[0], pos_ba[1], -2.f));

	static const rend::matx4 pr = rend::matx4().ortho(-1.f, 1.f, -1.f, 1.f, 1.f, 4.f);

	const rend::matx4 mvp_pa = rend::matx4().mul(pr, mv_pa).transpose();
	const rend::matx4 mvp_ba = rend::matx4().mul(pr, mv_ba).transpose();

	/////////////////////////////////////////////////////////////////

	glUseProgram(g_shader_prog[PROG_PADDLE]);

	if (-1 != g_uni[PROG_PADDLE][UNI_MVP])
	{
		glUniformMatrix4fv(g_uni[PROG_PADDLE][UNI_MVP], 1, GL_FALSE, reinterpret_cast< const GLfloat* >((const float(&)[4][4]) mvp_pa));
	}

	if (0 != g_tex[TEX_ALBEDO] && -1 != g_uni[PROG_PADDLE][UNI_SAMPLER_ALBEDO])
	{
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, g_tex[TEX_ALBEDO]);

		glUniform1i(g_uni[PROG_PADDLE][UNI_SAMPLER_ALBEDO], 0);
	}

	glBindBuffer(GL_ARRAY_BUFFER, g_vbo[0]);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_vbo[1]);

	st::setupVertexAttrPointers< st::Vertex >(g_active_attr_semantics[PROG_PADDLE]);

	for (unsigned i = 0; i < g_active_attr_semantics[PROG_PADDLE].num_active_attr; ++i)
		glEnableVertexAttribArray(g_active_attr_semantics[PROG_PADDLE].active_attr[i]);

	glDrawElements(GL_TRIANGLES, g_num_faces[0] * 3, GL_UNSIGNED_SHORT, 0);

	for (unsigned i = 0; i < g_active_attr_semantics[PROG_PADDLE].num_active_attr; ++i)
		glDisableVertexAttribArray(g_active_attr_semantics[PROG_PADDLE].active_attr[i]);

	/////////////////////////////////////////////////////////////////

	glUseProgram(g_shader_prog[PROG_BALL]);

	if (-1 != g_uni[PROG_BALL][UNI_SOLID_COLOR])
	{
		const GLfloat solid_color[4] = { 0.f, 1.f, 0.f, 1.f };

		glUniform4fv(g_uni[PROG_BALL][UNI_SOLID_COLOR], 1, solid_color);
	}

	if (-1 != g_uni[PROG_BALL][UNI_MVP])
	{
		glUniformMatrix4fv(g_uni[PROG_BALL][UNI_MVP], 1, GL_FALSE,
			reinterpret_cast< const GLfloat* >((const float(&)[4][4]) mvp_ba));
	}

	glBindBuffer(GL_ARRAY_BUFFER, g_vbo[2]);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_vbo[3]);

	st::setupVertexAttrPointers< st::Vertex >(g_active_attr_semantics[PROG_BALL]);

	for (unsigned i = 0; i < g_active_attr_semantics[PROG_BALL].num_active_attr; ++i)
		glEnableVertexAttribArray(g_active_attr_semantics[PROG_BALL].active_attr[i]);

	glDrawElements(GL_TRIANGLES, g_num_faces[1] * 3, GL_UNSIGNED_SHORT, 0);

	for (unsigned i = 0; i < g_active_attr_semantics[PROG_BALL].num_active_attr; ++i)
		glDisableVertexAttribArray(g_active_attr_semantics[PROG_BALL].active_attr[i]);

	/////////////////////////////////////////////////////////////////

	const rend::matx4 mv_pt =
		rend::matx4().scale(denom_pfield, denom_pfield, 1.f).mul(rend::matx4().translate(pos_pa[0], pos_pa[1], -2.f));

	glUseProgram(g_shader_prog[PROG_BALL]);

	glBindBuffer(GL_ARRAY_BUFFER, g_vbo[2]);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_vbo[3]);

	st::setupVertexAttrPointers< st::Vertex >(g_active_attr_semantics[PROG_BALL]);

	for (unsigned i = 0; i < g_active_attr_semantics[PROG_BALL].num_active_attr; ++i)
		glEnableVertexAttribArray(g_active_attr_semantics[PROG_BALL].active_attr[i]);

	if (-1 != g_uni[PROG_BALL][UNI_MVP])
	{
		const rend::matx4 mvp = rend::matx4().mul(pr, mv_pt).transpose();

		glUniformMatrix4fv(g_uni[PROG_BALL][UNI_MVP], 1, GL_FALSE,
			reinterpret_cast< const GLfloat* >((const float(&)[4][4]) mvp));
	}

	glDrawElements(GL_TRIANGLES, g_num_faces[1] * 3, GL_UNSIGNED_SHORT, 0);

	for (unsigned i = 0; i < g_active_attr_semantics[PROG_BALL].num_active_attr; ++i)
		glDisableVertexAttribArray(g_active_attr_semantics[PROG_BALL].active_attr[i]);

	/////////////////////////////////////////////////////////////////

	const mi::Vertex vtx[] =
	{
		{ { pos_pa[0], pos_pa[1], 0.f } },
		{ { pos_ba[0], pos_ba[1], 0.f } }
	};

	if (-1 != g_uni[PROG_BALL][UNI_MVP])
	{
		const rend::matx4 mvp = rend::matx4().translate(0.f, 0.f, -2.f).mul(pr).transpose();

		glUniformMatrix4fv(g_uni[PROG_BALL][UNI_MVP], 1, GL_FALSE,
			reinterpret_cast< const GLfloat* >((const float(&)[4][4]) mvp));
	}

	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

	mi::setupVertexAttrPointers< mi::Vertex >(g_active_attr_semantics[PROG_BALL], uintptr_t(vtx));

	for (unsigned i = 0; i < g_active_attr_semantics[PROG_BALL].num_active_attr; ++i)
		glEnableVertexAttribArray(g_active_attr_semantics[PROG_BALL].active_attr[i]);

	glDrawArrays(GL_LINES, 0, sizeof(vtx) / sizeof(vtx[0]));

	for (unsigned i = 0; i < g_active_attr_semantics[PROG_BALL].num_active_attr; ++i)
		glDisableVertexAttribArray(g_active_attr_semantics[PROG_BALL].active_attr[i]);

	/////////////////////////////////////////////////////////////////

	static mi::Vertex glyph[128];
	float offset = 0.f;
	unsigned nv = 0;

	char str[16];
	sprintf(str, "%.4u", g_score);

	bool success = false;

	success = string_numeral_to_vertices(
		str,
		sizeof(glyph) / sizeof(glyph[0]),
		glyph,
		nv,
		offset);

	assert(success);

	const unsigned nv_score = nv;

	offset += 2.f;
	sprintf(str, "%.10u", g_frame);

	success = string_numeral_to_vertices(
		str,
		sizeof(glyph) / sizeof(glyph[0]),
		glyph,
		nv,
		offset);

	assert(success);

	if (-1 != g_uni[PROG_BALL][UNI_MVP])
	{
		const rend::matx4 mvp = rend::matx4().scale(.0625f, .0625f, 1.f)
			.mul(rend::matx4().translate(-.875f, .75f, -2.f))
			.mul(pr).transpose();

		glUniformMatrix4fv(g_uni[PROG_BALL][UNI_MVP], 1, GL_FALSE,
			reinterpret_cast< const GLfloat* >((const float(&)[4][4]) mvp));
	}

	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

	mi::setupVertexAttrPointers< mi::Vertex >(g_active_attr_semantics[PROG_BALL], uintptr_t(glyph));

	for (unsigned i = 0; i < g_active_attr_semantics[PROG_BALL].num_active_attr; ++i)
		glEnableVertexAttribArray(g_active_attr_semantics[PROG_BALL].active_attr[i]);

	glDrawArrays(GL_LINES, 0, nv);

	for (unsigned i = 0; i < g_active_attr_semantics[PROG_BALL].num_active_attr; ++i)
		glDisableVertexAttribArray(g_active_attr_semantics[PROG_BALL].active_attr[i]);

	FalloutTarget fallout;
	static bool fallout_ongoing;
	static mi::Vertex fallout_glyph[128];

	if (g_fallout(fallout))
	{
		if (!fallout_ongoing)
		{
			fallout_ongoing = true;

			memcpy(fallout_glyph, glyph, sizeof(mi::Vertex) * nv_score);
		}

		if (-1 != g_uni[PROG_BALL][UNI_SOLID_COLOR])
		{
			const GLfloat solid_color[4] = { 0.f, fallout.luma, 0.f, 1.f };

			glUniform4fv(g_uni[PROG_BALL][UNI_SOLID_COLOR], 1, solid_color);
		}

		if (-1 != g_uni[PROG_BALL][UNI_MVP])
		{
			const rend::matx4 mvp = rend::matx4().scale(.0625f, .0625f, 1.f)
				.mul(rend::matx4().translate(-.875f, .75f - fallout.offs, -2.f))
				.mul(pr).transpose();

			glUniformMatrix4fv(g_uni[PROG_BALL][UNI_MVP], 1, GL_FALSE,
				reinterpret_cast< const GLfloat* >((const float(&)[4][4]) mvp));
		}

		glBindBuffer(GL_ARRAY_BUFFER, 0);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

		mi::setupVertexAttrPointers< mi::Vertex >(g_active_attr_semantics[PROG_BALL], uintptr_t(fallout_glyph));

		for (unsigned i = 0; i < g_active_attr_semantics[PROG_BALL].num_active_attr; ++i)
			glEnableVertexAttribArray(g_active_attr_semantics[PROG_BALL].active_attr[i]);

		glDrawArrays(GL_LINES, 0, nv_score);

		for (unsigned i = 0; i < g_active_attr_semantics[PROG_BALL].num_active_attr; ++i)
			glDisableVertexAttribArray(g_active_attr_semantics[PROG_BALL].active_attr[i]);
	}
	else
		if (fallout_ongoing)
		{
			fallout_ongoing = false;
		}

	return true;
}


static bool
intersect_segments(
	const rend::vect2& oa,
	const rend::vect2& va,
	const rend::vect2& ob,
	const rend::vect2& vb,
	rend::vect2& c)
{
	// no intersection if either of the segments is approaching zero length
	const float epsilon = 1e-7;

	if (epsilon > va.sqr() ||
		epsilon > vb.sqr())
	{
		return false;
	}

	// compute ax + by + c = 0 for the lines of both segments (just a and b here)
	const rend::vect2 na = rend::vect2(-va[1], va[0]).normalise();
	const rend::vect2 nb = rend::vect2(-vb[1], vb[0]).normalise();

	// compute implied end-points for both segments
	const rend::vect2 ea = rend::vect2().add(oa, va);
	const rend::vect2 eb = rend::vect2().add(ob, vb);

#if 0
	const float lb_dot_oa = nb.dot(oa) - nb.dot(ob);
	const float lb_dot_ea = nb.dot(ea) - nb.dot(ob);
	const float la_dot_ob = na.dot(ob) - na.dot(oa);
	const float la_dot_eb = na.dot(eb) - na.dot(oa);
#else
	float lb_dot_oa = nb.dot(oa) - nb.dot(ob);
	float lb_dot_ea = nb.dot(ea) - nb.dot(ob);
	float la_dot_ob = na.dot(ob) - na.dot(oa);
	float la_dot_eb = na.dot(eb) - na.dot(oa);

	const float eps = 1e-5;

	if (eps > fabsf(lb_dot_oa))
		lb_dot_oa = 0.f;

	if (eps > fabsf(lb_dot_ea))
		lb_dot_ea = 0.f;

	if (eps > fabsf(la_dot_ob))
		la_dot_ob = 0.f;

	if (eps > fabsf(la_dot_eb))
		la_dot_eb = 0.f;
#endif

	// check if both end points of a segment lie on the same side of the line of the other segment
	const unsigned side_a[][2] =
	{
		{
			0.f > lb_dot_oa ? 1 : 0,
			0.f > lb_dot_ea ? 1 : 0
		},
		{
			0.f < lb_dot_oa ? 1 : 0,
			0.f < lb_dot_ea ? 1 : 0
		}
	};

	const unsigned side_b[][2] =
	{
		{
			0.f > la_dot_ob ? 1 : 0,
			0.f > la_dot_eb ? 1 : 0
		},
		{
			0.f < la_dot_ob ? 1 : 0,
			0.f < la_dot_eb ? 1 : 0
		}
	};

	if (!(side_a[0][0] ^ side_a[0][1] || side_a[1][0] ^ side_a[1][1]) ||
		!(side_b[0][0] ^ side_b[0][1] || side_b[1][0] ^ side_b[1][1]))
	{
		return false;
	}

	const float tb = (va[0] * (ob[1] - oa[1]) - va[1] * (ob[0] - oa[0])) / (va[1] * vb[0] - va[0] * vb[1]);

	c = vb;
	c.mul(tb);
	c.add(ob);

	return true;
}


static void
impact_and_regen(
	unsigned xoffset)
{
	unsigned roffset = -1U;
	const int16_t regen_delay = 2048;
	const int16_t regen_speed = 1;

	for (unsigned i = 0; i < g_paddle_src_w; ++i)
		if (PADDLE_STATE_MUTABLE < g_paddle_state[i] &&
			PADDLE_STATE_MUTABLE == (g_paddle_state[i] -= regen_speed))
		{
			g_paddle_state[i] = PADDLE_STATE_IMMUTABLE;
			roffset = i;
		}

	if (-1 != xoffset &&
		PADDLE_STATE_MUTABLE == g_paddle_state[xoffset])
	{
		g_cooldown.setTransient(10);
		g_paddle_state[xoffset] = regen_delay;

		g_score += g_score_step;
		++g_hits;
	}
	else
		xoffset = -1U;

	if (-1 != xoffset ||
		-1 != roffset)
	{
		glBindTexture(GL_TEXTURE_2D, g_tex[TEX_ALBEDO]);
	}

	if (-1 != xoffset)
	{
		const util::pix hole(0, 0, 0);

		glTexSubImage2D(GL_TEXTURE_2D, 0, xoffset, 0, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &hole);
	}

	if (-1 != roffset)
	{
		const util::pix immut(255, 255, 255);

		glTexSubImage2D(GL_TEXTURE_2D, 0, roffset, 0, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &immut);
	}
}


bool
hook::input_frame(
	const GLuint input)
{
	const float ball_min_speed = 1.f / 16.f;
	const float ball_max_speed = 1.f / 4.f;

	static float ball_speed = ball_min_speed;
#if 1
	static int ball_step[2] = { -1, 1 };
#else
	static int ball_step[2] = { 1, 1 };
#endif

	if (input & INPUT_MASK_ESC)
		return false;

	int tent_pos_paddle[2] =
	{
		g_pos_paddle[0],
		g_pos_paddle[1]
	};

	if (input & INPUT_MASK_LEFT)
		--tent_pos_paddle[0];

	if (input & INPUT_MASK_RIGHT)
		++tent_pos_paddle[0];

	if (input & INPUT_MASK_DOWN)
		--tent_pos_paddle[1];

	if (input & INPUT_MASK_UP)
		++tent_pos_paddle[1];

	const int paddle_step[2] =
	{
		tent_pos_paddle[0] - g_pos_paddle[0],
		tent_pos_paddle[1] - g_pos_paddle[1]
	};

	const float tent_pos_ball[2] =
	{
		g_pos_ball[0] + ball_speed * ball_step[0],
		g_pos_ball[1] + ball_speed * ball_step[1]
	};

	const int paddle_ext = g_paddle_size / 2;
	unsigned xoffset = -1U;

	const rend::vect2& ob = rend::vect2::cast(g_pos_ball);
	const rend::vect2 vb = rend::vect2(tent_pos_ball).sub(ob);
	const rend::vect2 vp = rend::vect2(paddle_step[0], paddle_step[1]).negate();

	const float maddle_side[4][2] =
	{
		{ g_pos_paddle[0] + paddle_ext, g_pos_paddle[1] + paddle_ext },
		{ g_pos_paddle[0] - paddle_ext, g_pos_paddle[1] + paddle_ext },
		{ g_pos_paddle[0] - paddle_ext, g_pos_paddle[1] - paddle_ext },
		{ g_pos_paddle[0] + paddle_ext, g_pos_paddle[1] - paddle_ext }
	};

	for (unsigned i = 0; i < 4; ++i)
	{
		const rend::vect2& oa = rend::vect2::cast(maddle_side[i]);
		const rend::vect2 va = rend::vect2(maddle_side[(i + 1) & 3]).sub(oa);

		const unsigned ii = i & 1;
		const unsigned ci = ii ^ 1;

		rend::vect2 vb;
		vb.set(ii, 0.f);
		vb.set(ci, vp[ci]);

		rend::vect2 c;

		if (intersect_segments(
				oa, va,
				ob, vb, c))
		{
			xoffset = g_paddle_src_w / 4 * i + abs(int(floorf(c[ii] + .5f) - maddle_side[i][ii]));

			if (g_paddle_src_w == xoffset)
				xoffset = 0;

			assert(g_paddle_src_w > xoffset);

			if (PADDLE_STATE_MUTABLE >= g_paddle_state[xoffset])
			{
				tent_pos_paddle[ci] = g_pos_paddle[ci];
			}
		}
	}

	const float paddle_side[4][2] =
	{
		{ tent_pos_paddle[0] + paddle_ext, tent_pos_paddle[1] + paddle_ext },
		{ tent_pos_paddle[0] - paddle_ext, tent_pos_paddle[1] + paddle_ext },
		{ tent_pos_paddle[0] - paddle_ext, tent_pos_paddle[1] - paddle_ext },
		{ tent_pos_paddle[0] + paddle_ext, tent_pos_paddle[1] - paddle_ext }
	};

	for (unsigned i = 0; i < 4; ++i)
	{
		const rend::vect2& oa = rend::vect2::cast(paddle_side[i]);
		const rend::vect2 va = rend::vect2(paddle_side[(i + 1) & 3]).sub(oa);

		rend::vect2 c;

		if (intersect_segments(
				oa, va,
				ob, vb, c))
		{
			const unsigned ii = i & 1;

			xoffset = g_paddle_src_w / 4 * i + abs(int(floorf(c[ii] + .5f) - maddle_side[i][ii]));

			if (g_paddle_src_w == xoffset)
				xoffset = 0;

			assert(g_paddle_src_w > xoffset);

			if (PADDLE_STATE_MUTABLE >= g_paddle_state[xoffset])
			{
				const unsigned ci = ii ^ 1;

				if (paddle_step[ci] != ball_step[ci])
					ball_step[ci] = -ball_step[ci];

				g_pos_ball.set(0, floorf(c[0] + .5f));
				g_pos_ball.set(1, floorf(c[1] + .5f));
			}
		}
	}

	impact_and_regen(xoffset);

	g_pos_ball.add(rend::vect2(ball_step[0], ball_step[1]).mul(ball_speed));
	g_pos_paddle = rend::ivect2::cast(tent_pos_paddle);

	if (fabsf(g_pos_ball[0]) > float(g_playfield_size / 2) ||
		fabsf(g_pos_ball[1]) > float(g_playfield_size / 2))
	{
		ball_speed = ball_min_speed;
		g_score_step = 1;

		g_pos_paddle = rend::ivect2(0, 0);
		g_pos_ball = rend::vect2(0.f, 0.f);

		g_fallout.setTransient(60);
		g_score /= 2;
	}

	if (0 == ++g_frame % 1000)
	{
		if (ball_max_speed > ball_speed)
		{
			ball_speed *= 2.f;
			g_score_step += 1;
		}

		if (g_paddle_src_w - 4 != g_hits &&
			g_score)
		{
			--g_score;
		}
	}

	return true;
}

} // namespace testbed
